ЛЕКЦИЯ 7

Адреса и указатели.

 

Статически выделяемая память

 

Для того, чтобы лучше понять специфику динамически выделяемой памяти, рассмотрим сначала ее "антипод" - память, распределяемую статически.

 

Такое выделение памяти используется всякий раз при объявлении "обычных" переменных в разделе var. Каждая переменная обладает двумя атрибутами: именем и описанием.

 

var a: integer;

 

Описание переменной нужно для того, чтобы компилятор знал, сколько ячеек памяти необходимо выделить для ее хранения. Память под статическую переменную выделяется один раз (до начала работы программы), и затем до конца работы выделенная область памяти считается "занятой" - никакая другая информация не может быть записана в эту ячейку.

 

Имя переменной позволяет обращаться в процессе работы программы к той ячейке памяти, которая была выделена под эту переменную на этапе компиляции.

 

Адреса

 

Имя переменной является ее своеобразным (буквенным) адресом. Однако у любой переменной есть также и обычный (цифровой или физический) адрес: номер ячейки, выделенной под эту переменную.

 

При страничной организации памяти адреса являются составными и состоят из номера сегмента памяти и смещения ячейки относительно начала этого сегмента.

 

Лучшая иллюстрация страничной организации памяти компьютера - это страничная организация любой печатной книги. Для того чтобы найти нужную строчку, нет необходимости задавать ее номер, считая от начала текста. Вместо этого можно задать сначала номер страницы (= сегмент) и только затем номер строки, считая от начала этой страницы (= смещение).

 

Для обращения к статически заданной переменной можно использовать как ее имя, объявленное в разделе var, так и ее физический адрес.

 

Например, "адрес" одной и той же географической точки можно записать по-разному: "49°47' северной широты и 86°36' восточной долготы" или просто "вершина пика Белуха Восточная".

 

Указатели

 

Для того чтобы хранить (цифровые) адреса, нужны особые переменные. Их называют указателями и относят к специальному типу данных.

 

Описание указателей

 

При описании типизированного указателя необходимо сообщить компилятору, адреса переменных какого типа он может хранить:

 

var <имя_указателя>: ^<тип_адресуемой_переменной>;

 

Например:

 

var p: ^integer;

   q: ^real;

   s: ^array[1..10] of byte;

 

Кроме того, существуют универсальные нетипизированные указатели, которые могут хранить адрес переменной любого типа:

 

var <имя_указателя>: pointer;

 

Операции с указателями

 

Определение адреса

 

Физический адрес любой переменной можно узнать при помощи стандартной функции addr(<имя_переменной>):<указатель> или унарной операции @<имя_переменной>.

 

В зависимости от значения директивы компилятора {$T}, результатом операции @ будет либо типизированный указатель (если установлено {$T+}), тип которого будет определен в соответствии с типом использованной переменной, либо нетипизированный указатель pointer (если установлено {$T-}).

 

Результат функции addr() совместим с указателями любых типов:

 

p:= addr(x); {x: real; p: ^byte)

 

Разыменование

 

Для того чтобы воспользоваться значением, хранящимся по некоторому адресу, необходимо его оттуда "извлечь". Унарная операция ^ называется разыменованием и записывается по следующему шаблону:

 

 <имя_указателя>^

 

Результатом операции ^ является значение, хранящееся по указанному адресу. Тип этого значения будет определяться типом (типизированного) указателя. К нетипизированным указателям операцию разыменования применять нельзя.

 

Из-за вольностей, допускаемых процедурой addr(), при разыменовании порой могут возникнуть забавные ситуации. Например, в результате выполнения такой вот программы:

 

const a: array[1..3] of char ='ААА'; {код(А)=128 или 01000000}

var p: ^word;

begin p:= addr(a);

         writeln(p^)

end

 

на экран будет выведено 32896, что в двоичной системе счисления выглядит как 01000000.01000000 (точкой помечена граница двух байтов). Иными словами, коды двух первых букв оказались слитыми в значение типа word.

 

Замечание: Операции @ и ^ являются взаимно обратными, то есть для любой переменной a и для любого типизированного указателя p верны следующие равенства:

 

@(p^)= p и (@a)^ = a

 

Присваивания

 

Для указателей действуют гораздо более жесткие правила совместимости типов, чем для обычных переменных. В операции присваивания могут фигурировать только указатели, адресующие переменные одинаковых типов данных. Нельзя, скажем, записать

 

p:= q; {p:= ^integer; q: ^byte}

 

Обойти эти ограничения позволяет универсальность нетипизированного указателя pointer, совместимого с указателями любых типов:

 

{p:= ^integer; q: ^byte; t: pointer}

t:= q;

p:= t;

 

У указателей также существует свой "ноль", который означает, что указатель не указывает никуда:

 

p:= nil;

 

Замечание: Если указатель не хранит конкретного адреса (его значение не определено), то это вовсе не означает, что он никуда не указывает. Скорее всего, он все-таки указывает, но только в какую-нибудь неожиданную (хорошо, если не системную) область памяти.

 

Сравнения

 

Для указателей определены две операции сравнения: = и <>.

 

Две переменные, описанные как указатели на один и тот же тип данных, считаются совпадающими, если они указывают на одну и ту же область памяти.

 

Для разнотипных указателей сравнения невозможны: попытка записать

 

if p = q then writeln('yes'); {p: ^byte; q: ^integer}

 

вызовет ошибку уже на этапе компиляции.

 

Однако сравнивать типизированный и нетипизированный указатели можно.

 

Динамически распределяемая память

 

Поскольку к любой переменной можно обратиться двояко - по имени и по адресу, - есть возможность сократить эту избыточность и оставить только один способ. Как мы уже видели, наличие имени означает и наличие адреса. Иными словами, если вы дали переменной имя в разделе var, то в процессе компиляции у нее появится и адрес.

 

Итак, пусть у некоторой переменной нет имени. Тем не менее можно расположить ее в памяти, выделив под нее необходимое количество байтов, и т.д. У переменной будет адрес, будет значение, но не будет имени. Следовательно, обратиться к такой переменной можно будет только с помощью указателя.

 

"Безымянные" переменные отличаются от "нормальных" переменных:

Нет имени - нечего описывать в разделе var.

Ничего не описано, значит, на этапе компиляции память под переменную не выделена. Следовательно, необходима возможность выделять память (и отменять это выделение) прямо в процессе работы программы. Именно из-за этой гибкости такие переменные и называют динамическими.

 

Если "потерять" указатель на переменную, то переменная останется недоступным "мусором", занимая место в памяти, вплоть до конца работы программы.

 

Динамическое выделение памяти

 

Типизированные указатели

 

Для выделения памяти служит стандартная процедура new():

 

new(<имя_указателя>);

 

Эта процедура ищет в незанятой памяти подходящий по размеру кусок и, "застолбив" это место для безымянной динамической переменной, записывает в типизированный указатель адрес выделенного участка. Поэтому часто говорят, что процедура new() создает динамическую переменную. Размер выделяемого "куска памяти" напрямую зависит от типа указателя.

 

Например, если переменная p была описана как указатель на integer-переменную, то процедура new(p) выделит два байта; под real-переменную необходимо выделить четыре байта и т.д.

 

Нетипизированные указатели

 

Для того чтобы выделить память, на которую будет указывать нетипизрованный указатель pointer, нужно воспользоваться стандартной процедурой getmem(p: pointer; size: word), которая выделит столько байт свободной памяти, сколько указано в переменной size.

 

Динамическое освобождение памяти

 

Типизированные указатели

 

Для уничтожения динамической переменной, то есть для освобождения занимаемой ею памяти, предназначена стандартная процедура

 

dispose(<имя_типизир_указателя>).

 

Процедура dispose() снимает пометку "занято" с определенного количества байтов, начиная с указанного адреса. Эта область памяти в дальнейшем считается свободной (хотя старое значение бывшей переменной в ней может некоторое время еще оставаться). Количество освобождаемых байтов определяется типом указателя p.

 

В результате освобождения памяти при помощи процедуры dispose() значение указателя, хранившего адрес освобожденной области, становится неопределенным. Во избежание проблем его лучше сразу же "обнулить":

 

dispose(p);

p:= nil;

 

Нетипизированные указатели

 

Для того чтобы освободить память, на которую указывает нетипизрованный указатель, нужно воспользоваться стандартной процедурой freemem(p: pointer; size: word), которая освободит в памяти столько байтов (начиная с указанного в переменной p адреса), сколько задано в переменной size.